1 module template_processor; 2 import std.typecons:Flag,Yes,No; 3 import std.file; 4 import std.json; 5 import std.path; 6 import std.uni; 7 8 9 /** 10 dub.template.json reference: 11 12 The params part is checked only once. Keep it at the top of the file. 13 For not conflicting with dub's internal parameters, it uses the syntax #PARAMETER 14 ```json 15 "params": { 16 "windows": { 17 //Defines windows specific parameters 18 }, 19 "linux": { 20 //Defines linux specific parameters 21 }, 22 "SOME_GLOBAL_VAR": "This parameter can be used anywhere here by simply using #SOME_GLOBAL_VAR" 23 } 24 ``` 25 26 A dub.template.json can have a parent dub.json(or dub.template.json), this is used for separating some 27 configurations, such as the release one, since things can get hairy quite fast if not done. 28 ```json 29 "$extends": "#HIPREME_ENGINE/dub.json" 30 ``` 31 32 ## Adding the engine optional modules 33 This can be done by using engineModules property. They will automatically 34 use the absolute path and be added to the linkedDependencies on the current section. It is checked on 35 both root and configurations. 36 37 Another important feature of it is that the engine distributed modules requires a special distribution of hipengine_api. 38 This distribution is hipengine_api:direct. This module optimizes the function calls to instead of using function pointers, 39 it uses extern definitions, this way, it can be built as a static library. 40 This way, it is checked inside the "release" configuration, for making every of them use the subConfiguration of 41 "direct". 42 43 ```json 44 "engineModules": [ 45 "util", 46 "game2d", 47 "math" 48 ] 49 ``` 50 51 Those in linkedDependencies will automatically be added a linker flag called 52 /WHOLEARCHIVE:depName for windows on ldc compiler. 53 Since this is an error prone operation, it may be handled by the templater. 54 Also checked in configurations. 55 ```json 56 "linkedDependencies": { 57 "someDubDep": {"path": "the/path/to/dep"}, 58 "arsd:anything": "11.0" 59 } 60 ``` 61 62 Those in unnamed dependencies will automatically be added to the "dependencies" section. 63 If the path does not exists, it will be ignored and simply do nothing. Also checked in configurations. 64 65 ```json 66 "unnamedDependencies": [ 67 "some/path/to/dep" 68 ] 69 ``` 70 */ 71 72 73 enum string templateName = "dub.template.json"; 74 75 private immutable string[] systems = 76 [ 77 "windows", 78 "linux" 79 ]; 80 81 private enum VariableType 82 { 83 _default, 84 currentSystem, 85 otherSystem 86 } 87 88 private VariableType getType(string keyName) 89 { 90 import std.algorithm.searching : countUntil; 91 string currentSystem = "unknown"; 92 version(Windows) 93 currentSystem = "windows"; 94 else version(Posix) 95 currentSystem = "linux"; 96 97 if(keyName == currentSystem) 98 return VariableType.currentSystem; 99 else if(systems.countUntil(keyName) != -1) 100 return VariableType.otherSystem; 101 return VariableType._default; 102 } 103 104 bool moduleHasDirect(string moduleName) 105 { 106 switch(moduleName) 107 { 108 case "game2d":return true; 109 default: return false; 110 } 111 } 112 113 114 /** 115 * 116 * Params: 117 * str = Any string 118 * start = Where the check will start 119 * varName = Out variable containing the variable name found 120 * Returns: The index where the search stopped 121 */ 122 private long getVariableName(in string str, long start, out string varName) 123 { 124 assert(str[start] == '#'); 125 long curr = start+1; 126 while(curr < str.length) 127 { 128 char ch = str[curr]; 129 if(!(ch.isNumber || ch.isAlpha || ch == '_')) 130 break; 131 curr++; 132 } 133 varName = str[start+1..curr]; 134 return curr; 135 } 136 137 138 private string processString(JSONValue json, string str) 139 { 140 import std.exception:enforce; 141 string returnString; 142 size_t lastStop = 0; 143 for(size_t i = 0; i < str.length; i++) 144 { 145 if(str[i] == '#') 146 { 147 returnString~= str[lastStop..i]; 148 string varName; 149 i = getVariableName(str, i, varName); 150 enforce(varName in json["params"], "Variable "~varName~" not found"); 151 returnString~= json["params"][varName].str; 152 lastStop = i; 153 i--; //For not updating too much 154 } 155 } 156 if(lastStop != str.length) returnString~= str[lastStop..$]; 157 return returnString; 158 } 159 160 /** 161 * 162 * Params: 163 * f = The file 164 * variables = Variables to replace in the #VARIABLE text. 165 * Returns: File with replaced text. 166 */ 167 private string processFile(string f, string[string] variables) 168 { 169 string output = ""; 170 size_t lastStop = 0; 171 for(size_t i = 0; i < f.length; i++) 172 { 173 if(f[i] == '#') 174 { 175 output~= f[lastStop..i]; 176 string varName; 177 i = getVariableName(f, i, varName); 178 assert(varName in variables, "Variable "~varName~" not found"); 179 output~= escapeWindowsSep(variables[varName]); 180 lastStop = i; 181 i--; //For not updating too much 182 } 183 } 184 if(lastStop != f.length) output~= f[lastStop..$]; 185 return output; 186 } 187 188 /** 189 * 190 * Params: 191 * json = The parsed dub.template.json 192 * Returns: The variables inside "params". 193 */ 194 private string[string] getParamsInTemplate(JSONValue json) 195 { 196 string[string] variables; 197 if(const(JSONValue)* params = "params" in json) 198 { 199 foreach(key, value; params.object) 200 { 201 switch(getType(key)) 202 { 203 case VariableType.currentSystem: 204 { 205 foreach(sysKey, sysValue; value.object) 206 variables[sysKey] = sysValue.str; 207 break; 208 } 209 case VariableType._default: 210 { 211 if((key in variables) is null) 212 variables[key] = value.str; 213 break; 214 } 215 default:break; 216 } 217 } 218 } 219 return variables; 220 } 221 222 private string escapeWindowsSep(string thePath) 223 { 224 string ret; 225 for(size_t i = 0; i < thePath.length; i++) 226 { 227 if(thePath[i] == '\\') 228 { 229 if(i+1 >= thePath.length || thePath[i+1] != '\\') 230 ret~= "\\\\"; 231 } 232 else 233 ret~= thePath[i]; 234 } 235 return ret; 236 } 237 238 /** 239 * Saves the current system variables in the cache. 240 * Saves the default type in the cache too. 241 * Params: 242 * templatePath = Where the file containing the template json is. 243 * projectPath = The path where the project is contained. Used for the reserved #PROJECT 244 * enginePath = Path where the engine is located. Used for the reserved #HIPREME_ENGINE 245 * settings = Extra settings that will be processed inside the template. 246 * extraVariables = Optional variables which are always defined. 247 * Returns: THe resulting string 248 */ 249 private string processTemplateImpl(string templatePath, string projectPath, string enginePath, const AdditionalSetting[] settings, 250 in string[string] extraVariables) 251 { 252 string file = readText(templatePath); 253 JSONValue json = parseJSON(file); 254 string[string] variables = getParamsInTemplate(json); 255 string hipremeEngine = enginePath.absolutePath.escapeWindowsSep; 256 string project = projectPath.absolutePath.escapeWindowsSep; 257 if(!("params" in json)) 258 json.object["params"] = emptyObject; 259 json["params"].object["HIPREME_ENGINE"] = hipremeEngine; 260 json["params"].object["PROJECT"] = project; 261 foreach(k, v; extraVariables) json["params"].object[k] = v; 262 263 264 foreach(op; settings) 265 { 266 JSONValue inherited = emptyObject; 267 if(op.name in json) 268 { 269 inherited = json; 270 op.handler(json, emptyObject); 271 } 272 if("configurations" in json) 273 { 274 foreach(cfg; json["configurations"].array) 275 { 276 op.handler(cfg, inherited); 277 cfg.object.remove(op.name); 278 } 279 } 280 if(op.name in json) 281 { 282 json.object.remove(op.name); 283 } 284 } 285 variables["PROJECT"] = projectPath.absolutePath.escapeWindowsSep; 286 variables["HIPREME_ENGINE"] = hipremeEngine; 287 ///Push the extra variables. 288 foreach(k, v; extraVariables) 289 variables[k] = v; 290 json.object.remove("params"); 291 json.object.remove("$schema"); 292 file = processFile(json.toPrettyString(JSONOptions.doNotEscapeSlashes), variables); 293 return file; 294 } 295 296 297 private struct AdditionalSetting 298 { 299 string name; 300 JSONValue delegate(JSONValue dubFile, JSONValue inherited = emptyObject) handler; 301 Flag!"configAvailable" config = Yes.configAvailable; 302 } 303 private enum emptyObject = JSONValue(string[string].init); 304 private enum emptyArray = JSONValue(JSONValue[].init); 305 306 enum TemplateProcessorResult 307 { 308 notFound, 309 invalid, 310 success 311 } 312 313 JSONValue getDubFromTemplate(string templatePath, string enginePath) 314 { 315 string out_jsonFile; 316 if(processTemplate(templatePath, enginePath, out_jsonFile) != TemplateProcessorResult.success) 317 throw new JSONException("Could not succesfully process template at path "~templatePath); 318 return parseJSON(out_jsonFile); 319 } 320 321 /** 322 * 323 * Params: 324 * templatePath = path/to/folder/with/dub.template.json 325 * enginePath = The engine path which will be used for the configuration engineModules 326 * templateResult = The resulting string which can be used to cache internally or even save a file. 327 * additionalVariables = Additional variables that may come as an always defined. Used internally 328 * Returns: The result of the operation 329 */ 330 TemplateProcessorResult processTemplate(string templatePath, string enginePath, out string templateResult, 331 in string[string] additionalVariables = string[string].init) 332 { 333 string processedPath = templatePath; 334 processedPath = processedPath.absolutePath; 335 if(!exists(templatePath)) 336 { 337 templateResult = "Path received '" ~ templatePath ~"' does not exists"; 338 return TemplateProcessorResult.notFound; 339 } 340 templatePath = buildPath(templatePath, templateName); 341 if(!exists(templatePath)) 342 { 343 templateResult = "File "~ templatePath~ " does not exists"; 344 return TemplateProcessorResult.notFound; 345 } 346 AdditionalSetting[] additionals = [ 347 {"$extends", (JSONValue json, JSONValue inherited) 348 { 349 import std.exception:enforce; 350 if(!("$extends" in json)) 351 return json; 352 string parentDub = json["$extends"].str; 353 string[] options = [ 354 parentDub, 355 buildPath(parentDub, "dub.json"), 356 buildPath(parentDub, "dub.template.json") 357 ]; 358 string[] excludeKeys = ["configurations", "subPackages"]; 359 JSONValue parentJson; 360 foreach(i, opt; options) 361 { 362 opt = processString(json, opt); 363 enforce(opt != templatePath, "Parent can't point to itself."); 364 if(exists(opt)) 365 { 366 if(i == 2) 367 parentJson = getDubFromTemplate(opt, enginePath); 368 else 369 parentJson = parseJSON(cast(string)read(opt)); 370 break; 371 } 372 } 373 import std.conv:to; 374 enforce(parentJson != JSONValue.init, "Could not find json in paths "~options.to!string); 375 foreach(key, value; parentJson.object) 376 { 377 import std.algorithm.searching : countUntil; 378 if(excludeKeys.countUntil(key) == -1) 379 { 380 if(!(key in json)) json.object[key] = parentJson[key]; 381 else 382 { 383 enforce(parentJson[key].type == json[key].type); 384 //New values that aren't array or object will be overridden 385 switch(json[key].type) 386 { 387 case JSONType.array: 388 { 389 JSONValue[] arr = parentJson[key].array; 390 foreach(parentValue; arr) 391 json[key].array ~= parentValue; 392 break; 393 } 394 case JSONType.object: 395 { 396 foreach(parentKey, parentValue; parentJson[key].object) 397 { 398 if(!(parentKey in json[key])) 399 json[key].object[parentKey] = parentValue; 400 } 401 break; 402 } 403 //If both define, child json overrides it. 404 default: continue; 405 } 406 } 407 } 408 } 409 410 return json; 411 }, No.configAvailable}, 412 {"engineModules", (JSONValue json, JSONValue inherited) 413 { 414 if("engineModules" in json) 415 foreach(mod; json["engineModules"].array) 416 { 417 if(!("linkedDependencies" in json)) 418 json.object["linkedDependencies"] = emptyObject; 419 json["linkedDependencies"].object[mod.str] = ["path": buildPath(enginePath, "modules", mod.str)]; 420 } 421 if(json["name"].str == "release") 422 { 423 if(!("subConfigurations" in json)) 424 json["subConfigurations"] = emptyObject; 425 426 static void putDirectSubconfiguration(ref JSONValue input, ref JSONValue fromCfg) 427 { 428 if("engineModules" in fromCfg) 429 foreach(mod; fromCfg["engineModules"].array) 430 { 431 if(moduleHasDirect(mod.str)) 432 input["subConfigurations"][mod.str] = "direct"; 433 } 434 } 435 ///Put direct from inherited 436 putDirectSubconfiguration(json, inherited); 437 putDirectSubconfiguration(json, json); 438 } 439 return json; 440 }}, 441 {"linkedDependencies", (JSONValue json, JSONValue inherited) 442 { 443 if(!("linkedDependencies" in json)) 444 return json; 445 foreach(key, value; json["linkedDependencies"].object) 446 { 447 if(!("dependencies" in json)) 448 json.object["dependencies"] = emptyObject; 449 if(!("lflags-windows-ldc" in json)) 450 json.object["lflags-windows-ldc"] = emptyArray; 451 json["dependencies"].object[key] = value; 452 json["lflags-windows-ldc"].array ~= JSONValue("/WHOLEARCHIVE:"~key); 453 } 454 return json; 455 }}, 456 {"unnamedDependencies", (JSONValue json, JSONValue inherited) 457 { 458 if(!("unnamedDependencies" in json)) 459 return json; 460 foreach(unnamedDep; json["unnamedDependencies"].array) 461 { 462 import std.stdio; 463 import std.exception:enforce; 464 string endingPath; 465 JSONValue* subConfiguration; 466 if(unnamedDep.type == JSONType.object) 467 { 468 enforce("path" in unnamedDep, "Unnamed dependencies with type object must contain a \"path\""); 469 endingPath = unnamedDep["path"].str; 470 subConfiguration = ("subConfiguration" in unnamedDep); 471 if(subConfiguration && !("subConfigurations" in json)) 472 json.object["subConfigurations"] = emptyObject; 473 } 474 else 475 endingPath = unnamedDep.str; 476 477 endingPath = processString(json, endingPath); 478 import std.algorithm.searching : find; 479 480 string[] dubPath = find!((string f) => exists(f))( 481 [ 482 buildPath(processedPath, endingPath, "dub.json"), 483 buildPath(processedPath, endingPath, "dub.template.json") 484 ]); 485 486 if(dubPath.length) 487 { 488 if(!("dependencies" in json)) 489 json.object["dependencies"] = emptyObject; 490 JSONValue dubJson = parseJSON(readText(dubPath[0])); 491 string packageName = dubJson["name"].str; 492 enforce(!(packageName in json["dependencies"]), "Package "~packageName~" from path "~endingPath~" is already present in the dependencies."); 493 json["dependencies"][packageName] = ["path": endingPath]; 494 if(subConfiguration) 495 json["subConfigurations"].object[packageName] = subConfiguration.str; 496 } 497 else 498 writeln("Warning: Unnamed dependency at path ", endingPath, " not found"); 499 } 500 return json; 501 }} 502 ]; 503 504 templateResult = processTemplateImpl(templatePath, processedPath, enginePath, additionals, additionalVariables); 505 return TemplateProcessorResult.success; 506 }